Skip to content

feat: zk go deployer#20219

Open
ashitakah wants to merge 18 commits intoethereum-optimism:developfrom
defi-wonderland:feat/zk-go-deployer
Open

feat: zk go deployer#20219
ashitakah wants to merge 18 commits intoethereum-optimism:developfrom
defi-wonderland:feat/zk-go-deployer

Conversation

@ashitakah
Copy link
Copy Markdown
Contributor

Changed

  • upgrade.go
    Added support for GameTypeZKDisputeGame (type 10), including the ZKDisputeGameConfig struct and ABI encoding required for the OPCM V2 upgrade flow.

  • add_game_type.go
    Hooked ZKDisputeGameType into the game type registry for devstack environments.

  • chain_intent.go
    Introduced ZKDisputeGameParams and an optional ZKDisputeGame field on AdditionalDisputeGame.
    Added validation in Check() to reject any ZK game entry with:

    • zero Verifier address
    • zero AbsolutePrestate
      This prevents silently broken game deployments.
  • devfeatures.go
    Added ZKDisputeGameDevFlag.
    The flag is automatically OR-ed into the DevFeatureBitmap in pipeline/implementations.go whenever any chain intent includes an AdditionalDisputeGame with VMType == VMTypeZK.
    This ensures ZkDisputeGameImpl is deployed without manual bitmap configuration.

  • pipeline/implementations.go
    Auto-enables ZKDisputeGameDevFlag when a ZK game is present in any chain’s AdditionalDisputeGames, then forwards it to DeployImplementations.

  • DeployImplementations.s.sol / VerifyOPCM.s.sol

    • Plumbed zkDisputeGameImpl through the deploy script Implementations struct.
    • Added it to validatorGetterChecks to ensure it is verified against the container during deployment.
  • Remaining files
    Added plumbing to read and persist zkDisputeGameImpl from ReadImplementationAddresses into state.


Why the initial deployment files weren't touched

  • DeployOPChain.s.sol intentionally registers the ZK game slot with enabled: false.
  • ZK is not intended to be activated for new chains at deployment time.
  • Instead, it is enabled on existing chains through the OPCM V2 upgrade path.

@ashitakah
Copy link
Copy Markdown
Contributor Author

/ci authorize 3ad0694

@0xOneTony 0xOneTony marked this pull request as ready for review April 21, 2026 16:44
@0xOneTony 0xOneTony requested review from a team and smartcontracts April 21, 2026 16:44
@0xOneTony 0xOneTony requested review from a team as code owners April 21, 2026 16:44
Copy link
Copy Markdown
Contributor

@ajsutton ajsutton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good generally. It looks like we have added support for ZK to op-devstack but don't have any tests that actually enable it. Would be good to add one so that we actually exercise the op-deployer code in a real situation rather than just unit tests.

Auto-enabling the dev feature also seems a bit dangerous so likely should just depend on the user doing that explicitly.

Comment thread op-deployer/pkg/deployer/pipeline/implementations.go Outdated
@ashitakah
Copy link
Copy Markdown
Contributor Author

Looks good generally. It looks like we have added support for ZK to op-devstack but don't have any tests that actually enable it. Would be good to add one so that we actually exercise the op-deployer code in a real situation rather than just unit tests.

Auto-enabling the dev feature also seems a bit dangerous so likely should just depend on the user doing that explicitly.

8736fc7

New tests are added

@ashitakah
Copy link
Copy Markdown
Contributor Author

/ci authorize d95d853

Copy link
Copy Markdown
Contributor

@ajsutton ajsutton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@ajsutton ajsutton enabled auto-merge April 22, 2026 23:46
@ajsutton ajsutton disabled auto-merge April 22, 2026 23:46

// zkGameArgEncoder encodes the ZK dispute game args for SetDisputeGameImpl.
// Mirrors the zkEncoder in upgrade/embedded/upgrade.go (same ABI signature).
var zkGameArgEncoder = w3.MustNewFunc("dummy((bytes32 absolutePrestate,address verifier,uint64 maxChallengeDuration,uint64 maxProveDuration,uint256 challengerBond))", "")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be wrong? ZK_ARGS_LENGTH is 172 but this encodes 160 bytes (32*5 bytes)? Am I missing something?

uint256 public constant ZK_ARGS_LENGTH = 172;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dug into this further, it is wrong, and it's not just a length mismatch. Walked through the full call chain and wrote a repro.

zkGameArgEncoder (dispute_games.go:265) is a w3 ABI tuple encoder, producing 5 × 32 = 160 bytes with per-field left-padding. The bytes flow through SetDisputeGameImpl.s.sol:75 into factory.setImplementation(gameType, impl, gameArgs) (DisputeGameFactory.sol:315-316), which stores them verbatim and appends them to every clone as CWIA immutable data (:210). ZKDisputeGame.sol:181-217 reads the packed layout at fixed byte offsets, and since LibGameArgs.sol:116 reverts with InvalidGameArgsLength on anything other than 172 bytes, any clone instantiated against this impl reverts on creation. Not silently broken, actively unusable.

The same tuple encoder works fine in upgrade/embedded/upgrade.go because OPCM re-abi.decodes and re-packs it on-chain via OPContractsManagerUtils.makeGameArgs (:455-467). The deployer path skips that step. The Cannon peer at dispute_games.go:189 uses gameargs.PackPermissionless(), which is the precedent to follow.

Also noticed the blob is missing three fields entirely: anchorStateRegistry, weth, and l2ChainId. Even if we switched to packed encoding without fixing that, we'd still be short, and those need to come from thisState.OpChainContracts and thisIntent.ID.

Wrote a quick repro: https://gist.github.com/wwared/75bf76f775a44410ade3fac80ce402d9

gh pr checkout 20219 --repo ethereum-optimism/optimism
curl -L https://gist.githubusercontent.com/wwared/75bf76f775a44410ade3fac80ce402d9/raw \
  -o op-deployer/pkg/deployer/pipeline/zk_gameargs_encoding_test.go
go test -run 'TestZKGameArgs_' -v ./op-deployer/pkg/deployer/pipeline/...

It encodes a known config via zkGameArgEncoder, strips the selector the same way dispute_games.go:157 does, and compares against the reference 172-byte packed layout. A PASS on TestZKGameArgs_DeployerEncoderProducesWrongLayout means the args are encoded incorrectly: each assertion pins one fact about the broken state (len == 160, verifier reads back as 0x000…0001111…1111 from the ABI pad, etc.). Once the fix lands the assertions flip, and at that point this should be replaced with a golden-byte positive-path test.

On why we didn't catch it earlier: the unit tests in dispute_games_test.go are all negative paths (…_ZeroImpl, …_NilParams, …_WrongDisputeGameType, …_NilChallengerBond, …_ZeroChallengerBond), which return before the encoder runs, so nothing inspects its output. TestEncodedUpgradeInputV2_GameArgsEncoding does pin the 160-byte ABI form byte-for-byte, but only for the upgrade path where OPCM re-packs. And the new smoke test in op-acceptance-tests/tests/proofs/zk/smoke_test.go routes through delegateCallWithSetCode on the OPCM impl (in sysgo/add_game_type.go), which is the upgrade path again. The new DeployAdditionalDisputeGames ZK branch has no coverage.

Fix-wise: swap the tuple encoder for a packed encoder matching OPContractsManagerUtils.makeGameArgs for ZK_DISPUTE_GAME (172 bytes, appending ASR/WETH/chainID), following the Cannon gameargs.PackPermissionless() pattern. Add a golden-byte positive-path test. And ideally a test that actually exercises the deployer path rather than only the upgrade flow.

Copy link
Copy Markdown
Contributor

@wwared wwared Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: it's entirely possible Claude got some stuff wrong in the above comment, but at the very least it does seem like the game args on the ZKDisputeGame.sol contract are different from the ones in this PR (missing the last three):

// - 0xAC gameArgs (absolutePrestate + verifier + durations + bond + registry + weth + l2ChainId)

Copy link
Copy Markdown
Contributor Author

@ashitakah ashitakah Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, fixed everything you flagged.

Swapped the w3 ABI tuple encoder for a proper ZKGameArgs.Pack() in the gameargs package (same pattern as PackPermissionless() for Cannon), now produces the correct 172 packed bytes. Also added the three missing fields (anchorStateRegistry, weth, l2ChainId) from thisState.OpChainContracts and thisIntent.ID, plus a test pinning every offset.

The end-to-end acceptance test for the deployer path is a follow-up, agreed it's missing but I will leave it as a separate issue.

@ashitakah
Copy link
Copy Markdown
Contributor Author

/ci authorize 991e973

@0xOneTony
Copy link
Copy Markdown
Contributor

/ci authorize 3f89c65

MaxChallengeDuration uint64 `json:"maxChallengeDuration" toml:"maxChallengeDuration"`
MaxProveDuration uint64 `json:"maxProveDuration" toml:"maxProveDuration"`
ChallengerBond *hexutil.Big `json:"challengerBond" toml:"challengerBond"`
InitBond *hexutil.Big `json:"initBond" toml:"initBond"`
Copy link
Copy Markdown
Contributor

@wwared wwared Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This InitBond value isn't ever being read from this struct I think, is it? If I'm correct it's only being read from the DisputeGameConfig.InitBond field.

If it's not, we should either wire it up in the upgrade path or remove it completely.

I think it's confusing to have InitBond in multiple places, so we should only have it in either DisputeGameConfig or in ZKDisputeGameParams but not both.

MaxProveDuration: zk.MaxProveDuration,
ChallengerBond: zk.ChallengerBond.ToInt(),
AnchorStateRegistry: thisState.OpChainContracts.AnchorStateRegistryProxy,
Weth: thisState.OpChainContracts.DelayedWethPermissionlessGameProxy,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/question: Here we use thisState.OpChainContracts.DelayedWethPermissionlessGameProxy (makes sense, ZK is permissionless), but in line 202 below, we use thisState.OpChainContracts.DelayedWethPermissionedGameProxy for both permissioned and permissionless cannon games in the deploy.

This question is not about a change in this PR (this behavior was already in op-deployer previously), and I think it is correct to use the permissionless proxy for ZK games, but I'm raising this inconsistency/difference so someone with more context around these contracts can double check (cc @stevennevins)

}
if game.ZKDisputeGame.AbsolutePrestate == (common.Hash{}) {
return fmt.Errorf("%w: AbsolutePrestate must not be zero, chainId=%s", ErrZKDisputeGameMissingParams, c.ID)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend we add the following checks to this block as well:

  if game.ZKDisputeGame.MaxChallengeDuration == 0 {
      return fmt.Errorf("%w: MaxChallengeDuration must be > 0, chainId=%s", ErrZKDisputeGameMissingParams, c.ID)
  }
  if game.ZKDisputeGame.MaxProveDuration == 0 {
      return fmt.Errorf("%w: MaxProveDuration must be > 0, chainId=%s", ErrZKDisputeGameMissingParams, c.ID)
  }
  if game.ZKDisputeGame.ChallengerBond == nil || game.ZKDisputeGame.ChallengerBond.ToInt().Sign() <= 0 {
      return fmt.Errorf("%w: ChallengerBond must be set to a positive value, chainId=%s", ErrZKDisputeGameMissingParams, c.ID)
  }

The first two are new checks, the third one is checked at deployDisputeGame time I think, but it would be good to check it earlier at intent time too.

I don't think we have these checks on the smart contracts, so we should have them on the Go deployer side

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants